﻿#region << 版 本 注 释 >>
/*----------------------------------------------------------------
 * 版权所有 (c) 2022 北京超维景生物科技有限公司 保留所有权利。
 * 
 * 创建者：huangyang
 * 电子邮箱：huangyang@tvscope.cn
 * 创建时间：2023/3/14 9:28:48
 * 版本：V1.0.0
 * 描述：
 *
 * ----------------------------------------------------------------
 * 修改人：
 * 时间：
 * 修改说明：
 *
 * 版本：V1.0.1
 *----------------------------------------------------------------*/
#endregion << 版 本 注 释 >>

using System.Text;
using ImageK.Gui;
using ImageK.IO;
using ImageK.Plugin.Frame;
using ImageK.Process;
using ImageK.Util;

namespace ImageK.Plugin.Filter
{
	/** This plugin convolves images using user user defined kernels. */
	public class Convolver : BasePlugInFilter,ExtendedPlugInFilter {
        /** Set this flag if the last preview image may be kept as a result.
	        For stacks, this flag can lead to out-of-sequence processing of the
	        slices, irrespective of the <code>PARALLELIZE_STACKS<code> flag.
         */
        public static int KEEP_PREVIEW = 0x1000000;

		private ImagePlus imp;
	    private int kw, kh;
	    private bool canceled;
	    private float[] kernel;
	    private bool isLineRoi;
	    private Button _open, _save;
	    private GenericDialog gd;
	    private MultiLineLabel messageLabel;
	    private bool normalize = true;
	    private int nSlices;
	    private int flags = DOES_ALL | CONVERT_TO_FLOAT | SUPPORTS_MASKING | KEEP_PREVIEW | FINAL_PROCESSING | SNAPSHOT;
	    private int nPasses = 1;
	    private bool kernelError;
	    private PlugInFilterRunner pfr;
	    private Thread mainThread;
	    private int pass;
	    private static string defaultKernelText = "-1 -1 -1 -1 -1\n-1 -1 -1 -1 -1\n-1 -1 24 -1 -1\n-1 -1 -1 -1 -1\n-1 -1 -1 -1 -1\n";
	    private static bool defaultNormalizeFlag = true;
	    private static string lastKernelText = defaultKernelText;
	    private static bool lastNormalizeFlag = defaultNormalizeFlag;
	    private string kernelText = defaultKernelText;
	    private bool normalizeFlag = defaultNormalizeFlag;

	    public override int setup(string arg, ImagePlus imp)
	    {
		    this.imp = imp;
		    mainThread = Thread.CurrentThread;
		    if (imp == null)
		    { IJ.noImage(); return DONE; }
		    if (arg.Equals("final") && imp.getRoi() == null)
		    {
			    imp.getProcessor().resetMinAndMax();
			    imp.updateAndDraw();
			    return DONE;
		    }
		    IJ.resetEscape();
		    Roi roi = imp.getRoi();
		    isLineRoi = roi != null && roi.isLine();
		    nSlices = imp.getStackSize();
		    if (imp.getStackSize() == 1)
			    flags |= PARALLELIZE_IMAGES;
		    else
			    flags |= PARALLELIZE_STACKS;
		    imp.startTiming();
		    return flags;
	    }

	    public override void run(ImageProcessor ip)
	    {
		    if (canceled) return;
		    if (isLineRoi) ip.resetRoi();
		    if (!kernelError)
			    convolve(ip, kernel, kw, kh);
	    }

	    public int showDialog(ImagePlus imp, String command, PlugInFilterRunner pfr)
	    {
		    bool interactive = Macro.GetOptions() == null;
		    if (interactive)
		    {
			    kernelText = lastKernelText;
			    normalizeFlag = lastNormalizeFlag;
		    }
		    gd = GUI.newNonBlockingDialog("Convolver...", imp);
		    // gd.setInsets(5, 20, 0);
		    gd.addMessage(" \nKernel:"); //reserve two lines
		    messageLabel = (MultiLineLabel)gd.getMessage();
		    // gd.setInsets(5, 20, 0);
		    gd.addTextAreas(kernelText, null, 10, 30);
		    gd.addPanel(makeButtonPanel(gd));
		    gd.addCheckbox("Normalize Kernel", normalizeFlag);
		    gd.addPreviewCheckbox(pfr);
		    // gd.addDialogListener(this);
		    gd.showDialog();
		    if (gd.wasCanceled())
			    return DONE;
		    this.pfr = pfr;
		    if (interactive)
		    {
			    lastKernelText = kernelText;
			    lastNormalizeFlag = normalizeFlag;
		    }
		    return IJ.setupDialog(imp, flags);
	    }

	    // public bool dialogItemChanged(GenericDialog gd, AWTEvent e)
	    // {
		   //  kernelText = gd.getNextText();
		   //  normalizeFlag = gd.getNextBoolean();
		   //  normalize = normalizeFlag;
		   //  kernelError = !decodeKernel(kernelText);
		   //  if (!kernelError)
		   //  {
			  //   IJ.ShowStatus("Convolve: " + kw + "x" + kh + " kernel");
			  //   return true;
		   //  }
		   //  else
			  //   return !gd.isPreviewActive();
	    // }

	    bool decodeKernel(String text)
	    {
		    if (Macro.GetOptions() != null && !hasNewLine(text))
			    return decodeSquareKernel(text);
            string[] rows = text.Trim().Split("\n");  //Tools.split(text.trim(), "\n");
		    kh = rows.Length;
		    if (kh == 0) return false;
            string[] values = rows[0].Trim().Split('\t', '\n', '\r'); //Tools.split(rows[0].Trim());

			kw = values.Length;
		    kernel = new float[kw * kh];
		    bool done = gd.wasOKed();
		    int i = 0;
		    String err = null;
		    for (int y = 0; y < kh; y++)
		    {
			    values = rows[y].Trim().Split('\t', '\n', '\r');  //Tools.split(rows[y]);
			    if (values.Length != kw)
				    err = "Row " + (y + 1) + " is not the same length\nas the first row";
			    else
				    for (int x = 0; x < kw; x++)
					    kernel[i++] = (float)Tools.parseDouble(values[x], 0.0);
		    }
		    if ((kw & 1) != 1 || (kh & 1) != 1)
			    err = "Kernel must have odd width and height.\nThis one is " + kw + "x" + kh + ".";
		    if (err == null)
		    {
			    messageLabel.setText(kw + "x" + kh + " kernel");
		    }
		    else
		    {
			    if (done)
				    IJ.error("Convolver", err);
			    else
				    messageLabel.setText(err);
			    return false;
		    }
		    return true;
	    }

	    bool hasNewLine(string text)
	    {
		    for (int i = 0; i < text.Length; i++)
		    {
			    if (text[i] == '\n') return true;
		    }
		    return false;
	    }

	    bool decodeSquareKernel(string text)
        {
            string[] values = text.Split('\t', '\n', '\r'); //Tools.split(text);
		    int n = values.Length;
		    kw = (int)Math.Sqrt(n);
		    kh = kw;
		    n = kw * kh;
		    kernel = new float[n];
		    for (int i = 0; i < n; i++)
			    kernel[i] = (float)Tools.parseDouble(values[i]);
		    if (kw >= 3 && (kw & 1) == 1)
		    {
			    StringBuilder sb = new StringBuilder();
			    int i = 0;
			    for (int y = 0; y < kh; y++)
			    {
				    for (int x = 0; x < kw; x++)
				    {
					    sb.Append("" + kernel[i++]);
					    if (x < kw - 1) sb.Append(" ");
				    }
				    sb.Append("\n");
			    }
			    kernelText = sb.ToString();
			    gd.getTextArea1().Text = sb.ToString();
			    return true;
		    }

            IJ.error("Kernel must be square with odd width. This one is " + kw + "x" + kh + ".");
            return false;
        }

	    /** Creates a panel containing "Save...", "Save..." and "Preview" buttons. */
	    Panel makeButtonPanel(GenericDialog gd)
	    {
		    FlowLayoutPanel buttons = new FlowLayoutPanel();
            buttons.FlowDirection = FlowDirection.LeftToRight;
            // buttons.setLayout(new FlowLayout(FlowLayout.CENTER, 5, 0));
		    _open = new Button()
            {
				Text = "Open..."
			};
		    // _open.addActionListener(this);
		    buttons.Controls.Add(_open);
		    _save = new Button()
            {
                Text = "Save..."
			};
		    // _save.addActionListener(this);
		    buttons.Controls.Add(_save);
		    return buttons;
	    }

	    /** Convolves <code>ip</code> with a kernel of width <code>kw</code> and
		    height <code>kh</code>. Returns false if the user cancels the operation. */
	    public bool convolve(ImageProcessor ip, float[] kernel, int kw, int kh)
	    {
		    if (canceled || kernel == null || kw * kh != kernel.Length)
			    return false;
		    if ((kw & 1) != 1 || (kh & 1) != 1)
			    throw new ArgumentException("Kernel width or height not odd (" + kw + "x" + kh + ")");
		    bool notFloat = !(ip is FloatProcessor);
		    ImageProcessor ip2 = ip;
		    if (notFloat)
		    {
			    if (ip2 is ColorProcessor)
				    throw new ArgumentException("RGB images not supported");
			    ip2 = ip2.convertToFloat();
		    }
		    if (kw == 1 || kh == 1)
			    convolveFloat1D((FloatProcessor)ip2, kernel, kw, kh, normalize ? getScale(kernel) : 1.0);
		    else
			    convolveFloat(ip2, kernel, kw, kh);
		    if (notFloat)
		    {
			    if (ip is ByteProcessor)
				    ip2 = ip2.convertToByte(false);
			    else
				    ip2 = ip2.convertToShort(false);
			    ip.setPixels(ip2.getPixels());
		    }
		    return !canceled;
	    }

	    /** If 'normalize' is true (the default), the convolve(), convolveFloat() and
		    convolveFloat1D() (4 argument version) methods divide each kernel
		    coefficient by the sum of the coefficients, preserving image brightness. */
	    public void setNormalize(bool normalizeKernel)
	    {
		    normalize = normalizeKernel;
	    }

	    /** Convolves the float image <code>ip</code> with a kernel of width
		    <code>kw</code> and height <code>kh</code>. Returns false if
		    the user cancels the operation by pressing 'Esc'. */
	    public bool convolveFloat(ImageProcessor ip, float[] kernel, int kw, int kh)
	    {
		    if (!(ip is FloatProcessor))
			    throw new ArgumentException("FloatProcessor required");
		    if (canceled) return false;
		    int width = ip.getWidth();
		    int height = ip.getHeight();
		    Rectangle r = ip.getRoi();
		    int x1 = r.X;
		    int y1 = r.Y;
		    int x2 = x1 + r.Width;
		    int y2 = y1 + r.Height;
		    int uc = kw / 2;
		    int vc = kh / 2;
		    float[] pixels = (float[])ip.getPixels();
		    float[] pixels2 = (float[])ip.getSnapshotPixels();
		    if (pixels2 == null)
			    pixels2 = (float[])ip.getPixelsCopy();
		    double scale = normalize ? getScale(kernel) : 1.0;
		    Thread thread = Thread.CurrentThread;
		    bool isMainThread = thread == mainThread || thread.Name.IndexOf("Preview") != -1;
		    if (isMainThread) pass++;
		    double sum;
		    int offset, i;
		    bool edgePixel;
		    int xedge = width - uc;
		    int yedge = height - vc;
		    long lastTime = DateTime.Now.Ticks;
		    for (int y = y1; y < y2; y++)
		    {
			    long time = DateTime.Now.Ticks;
			    if (time - lastTime > 100)
			    {
				    lastTime = time;
					//todo:
				    // if (thread.isInterrupted()) return false;
				    if (isMainThread)
				    {
					    if (IJ.escapePressed())
					    {
						    canceled = true;
						    ip.reset();
						    ImageProcessor originalIp = imp.getProcessor();
						    if (originalIp.getNChannels() > 1)
							    originalIp.reset();
						    return false;
					    }
					    showProgress((y - y1) / (double)(y2 - y1));
				    }
			    }
			    for (int x = x1; x < x2; x++)
			    {
				    if (canceled) return false;
				    sum = 0.0;
				    i = 0;
				    edgePixel = y < vc || y >= yedge || x < uc || x >= xedge;
				    for (int v = -vc; v <= vc; v++)
				    {
					    offset = x + (y + v) * width;
					    for (int u = -uc; u <= uc; u++)
					    {
						    if (edgePixel)
						    {
							    if (i >= kernel.Length) // work around for JIT compiler bug on Linux
								    IJ.log("kernel index error: " + i);
							    sum += getPixel(x + u, y + v, pixels2, width, height) * kernel[i++];
						    }
						    else
							    sum += pixels2[offset + u] * kernel[i++];
					    }
				    }
				    pixels[x + y * width] = (float)(sum * scale);
			    }
		    }
		    return true;
	    }

	    /** Convolves the image <code>ip</code> with a kernel of width
		    <code>kw</code> and height <code>kh</code>. */
	    public void convolveFloat1D(FloatProcessor ip, float[] kernel, int kw, int kh)
	    {
		    convolveFloat1D(ip, kernel, kw, kh, normalize ? getScale(kernel) : 1.0);
	    }

	    /** Convolves the image <code>ip</code> with a kernel of width
		    <code>kw</code> and height <code>kh</code>. */
	    public void convolveFloat1D(FloatProcessor ip, float[] kernel, int kw, int kh, double scale)
	    {
		    int width = ip.getWidth();
		    int height = ip.getHeight();
		    Rectangle r = ip.getRoi();
		    int x1 = r.X;
		    int y1 = r.Y;
		    int x2 = x1 + r.Width;
		    int y2 = y1 + r.Height;
		    int uc = kw / 2;
		    int vc = kh / 2;
		    float[] pixels = (float[])ip.getPixels();
		    float[] pixels2 = (float[])ip.getSnapshotPixels();
		    if (pixels2 == null)
			    pixels2 = (float[])ip.getPixelsCopy();
		    bool vertical = kw == 1;

		    double sum;
		    int offset, i;
		    bool edgePixel;
		    int xedge = width - uc;
		    int yedge = height - vc;
		    for (int y = y1; y < y2; y++)
		    {
			    for (int x = x1; x < x2; x++)
			    {
				    sum = 0.0;
				    i = 0;
				    if (vertical)
				    {
					    edgePixel = y < vc || y >= yedge;
					    offset = x + (y - vc) * width;
					    for (int v = -vc; v <= vc; v++)
					    {
						    if (edgePixel)
							    sum += getPixel(x + uc, y + v, pixels2, width, height) * kernel[i++];
						    else
							    sum += pixels2[offset + uc] * kernel[i++];
						    offset += width;
					    }
				    }
				    else
				    {
					    edgePixel = x < uc || x >= xedge;
					    offset = x + (y - vc) * width;
					    for (int u = -uc; u <= uc; u++)
					    {
						    if (edgePixel)
							    sum += getPixel(x + u, y + vc, pixels2, width, height) * kernel[i++];
						    else
							    sum += pixels2[offset + u] * kernel[i++];
					    }
				    }
				    pixels[x + y * width] = (float)(sum * scale);
			    }
		    }
	    }

	    public static double getScale(float[] kernel)
	    {
		    double scale = 1.0;
		    double sum = 0.0;
		    for (int i = 0; i < kernel.Length; i++)
			    sum += kernel[i];
		    if (sum != 0.0)
			    scale = 1.0 / sum;
		    return scale;
	    }

	    private float getPixel(int x, int y, float[] pixels, int width, int height)
	    {
		    if (x <= 0) x = 0;
		    if (x >= width) x = width - 1;
		    if (y <= 0) y = 0;
		    if (y >= height) y = height - 1;
		    return pixels[x + y * width];
	    }

	    void save()
	    {
			//todo:
		    // TextArea ta1 = gd.getTextArea1();
		    // ta1.selectAll();
		    // String text = ta1.getText();
		    // ta1.select(0, 0);
		    // if (text == null || text.length() == 0)
			   //  return;
		    // text += "\n";
		    // SaveDialog sd = new SaveDialog("Save as Text...", "kernel", ".txt");
		    // String name = sd.getFileName();
		    // if (name == null)
			   //  return;
		    // String directory = sd.getDirectory();
		    // PrintWriter pw = null;
		    // try
		    // {
			   //  FileOutputStream fos = new FileOutputStream(directory + name);
			   //  BufferedOutputStream bos = new BufferedOutputStream(fos);
			   //  pw = new PrintWriter(bos);
		    // }
		    // catch (IOException e)
		    // {
			   //  IJ.error("" + e);
			   //  return;
		    // }
		    // IJ.wait(250);  // give system time to redraw ImageJ window
		    // pw.print(text);
		    // pw.close();
	    }

	    void open()
	    {
		    OpenDialog od = new OpenDialog("Open Kernel...", "");
		    string directory = od.GetDirectory();
		    string name = od.GetFileName();
		    if (name == null)
			    return;
		    string path = directory + name;
		    TextReader tr = new TextReader();
		    ImageProcessor ip = tr.open(path);
		    if (ip == null)
			    return;
		    int width = ip.getWidth();
		    int height = ip.getHeight();
		    if ((width & 1) != 1 || (height & 1) != 1)
		    {
			    IJ.error("Convolver", "Kernel must be have odd width and height");
			    return;
		    }
		    StringBuilder sb = new StringBuilder();
		    bool integers = true;
		    for (int y = 0; y < height; y++)
		    {
			    for (int x = 0; x < width; x++)
			    {
				    double v = ip.getPixelValue(x, y);
				    if ((int)v != v)
					    integers = false;
			    }
		    }
		    for (int y = 0; y < height; y++)
		    {
			    for (int x = 0; x < width; x++)
			    {
				    if (x != 0) sb.Append(" ");
				    double v = ip.getPixelValue(x, y);
				    if (integers)
					    sb.Append(IJ.d2s(ip.getPixelValue(x, y), 0));
				    else
					    sb.Append("" + ip.getPixelValue(x, y));
			    }
			    if (y != height - 1)
				    sb.Append("\n");
		    }
		    gd.getTextArea1().Text =sb.ToString();
	    }

	    public void setNPasses(int nPasses)
	    {
		    this.nPasses = nPasses;
		    pass = 0;
	    }

	    private void showProgress(double percent)
	    {
		    percent = (double)(pass - 1) / nPasses + percent / nPasses;
		    IJ.showProgress(percent);
	    }

	    // public void actionPerformed(ActionEvent e)
	    // {
		   //  Object source = e.getSource();
		   //  Recorder.disablePathRecording();
		   //  if (source == save)
			  //   save();
		   //  else if (source == open)
			  //   open();
	    // }

}

}
